﻿using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows.Forms;
using SilverlightViewport.Common.Interop;

namespace SilverlightViewport.Common
{
    #region Event Args
    public class SilverlightErrorEventArgs : EventArgs
    {
        internal SilverlightErrorEventArgs()
        {}

        public string Filename { get; set; }
        public string Source { get; set; }
        public int Line { get; set; }
        public int Column { get; set; }
    }
    #endregion;

    public class SilverlightWindowless : WindowlessContainer, 
                                         IXcpControlHost, 
                                         Interop.IServiceProvider
                                         
    {
        #region NativeMethods
        [SuppressUnmanagedCodeSecurity]
        [DllImport("ole32.dll")]
        private static extern int OleDraw([MarshalAs(UnmanagedType.IUnknown)] object pUnk, uint dwAspect, IntPtr hdcDraw, [In] ref RECT lprcBounds);

        [SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32"), SuppressUnmanagedCodeSecurity]
        private static extern bool PostThreadMessage(int threadId, uint msg,
                                                    ushort wParam, uint lParam);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);

        [DllImport("user32.dll")]
        private static extern IntPtr DispatchMessage([In] ref Msg lpmsg);

        [DllImport("user32.dll")]
        static extern bool TranslateMessage([In] ref Msg lpMsg);

        [StructLayout(LayoutKind.Sequential)]
        private struct Msg
        {
            public IntPtr hwnd;
            public int message;
            public IntPtr wParam;
            public IntPtr lParam;
            public int time;
            public int pt_x;
            public int pt_y;
        }

        [DllImport("kernel32"), SuppressUnmanagedCodeSecurity]
        static extern int GetCurrentThreadId();

        [DllImport("ole32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
        public static extern int OleUninitialize();
       
        #endregion

        #region Local Fields
        /// <summary>
        /// Silverlight COM instance
        /// </summary>
        private IXcpControl m_silverlightControl;

        /// <summary>
        /// Ole verb for inplace activation
        /// </summary>
        private const int OLEIVERB_INPLACE_ACTIVATE = -5;

        /// <summary>
        /// Flag to detect if the size of the control has changed
        /// </summary>
        private bool m_controlSizeDirty = true;

        /// <summary>
        /// Base Url for Silverlight downloading
        /// </summary>
        private string m_baseUrl;

        /// <summary>
        /// XAML string for Silverlight to render
        /// </summary>
        private string m_xaml;

        /// <summary>
        /// Render Target Bitmap used to render Silverlight to a buffer
        /// </summary>
        private RenderTargetBitmap m_renderTargetBitmap;

        /// <summary>
        /// Pointer to the unmanaged buffer of the rendered frame
        /// </summary>
        private IntPtr m_pvBits;

        /// <summary>
        /// The SilverlightViewer interface.  Used to create RTBs
        /// </summary>
        private SilverlightViewer m_silverlightViewer;

        /// <summary>
        /// Timer to invoke rendering of a Silverlight frame
        /// </summary>
        private System.Windows.Forms.Timer m_renderTimer;

        /// <summary>
        /// Frames per second to render
        /// </summary>
        private int m_renderFps;

        /// <summary>
        /// The source url for Silverlight
        /// </summary>
        private string m_source = string.Empty;

        /// <summary>
        /// Bits per pixel of each frame
        /// </summary>
        private const int BITS_PER_PIXEL = 4;

        /// <summary>
        /// Name of the IDispatch property of the "Root" Silverlight element
        /// </summary>
        private const string ROOT_ELEMENT_PROPERTY_NAME = "Root";

        /// <summary>
        /// Xaml string for the container to use to hold custom XAML rendering (via Xaml property)
        /// </summary>
        private const string XAML_SOURCE_CONTAINER ="<ContentControl xmlns=\"http://schemas.microsoft.com/client/2007\"></ContentControl>";

        private IntPtr m_handle;

        #endregion

        public SilverlightWindowless()
        {
            HandleDestroyed += SilverlightWindowless_HandleDestroyed;
        }

        public event EventHandler<SilverlightErrorEventArgs> SilverlightError;

        public event EventHandler<RenderedFrameEventArgs> OnRendered;

        private void InvokeOnRendered(RenderedFrameEventArgs e)
        {
            EventHandler<RenderedFrameEventArgs> rendered = OnRendered;
            if (rendered != null) rendered(this, e);
        }

        private void InvokeSilverlightError(SilverlightErrorEventArgs e)
        {
            EventHandler<SilverlightErrorEventArgs> error = SilverlightError;
            if (error != null) error(this, e);
        }

        #region Helpers
        public static IntPtr MakeLParam(int wLow, int wHigh)
        {
            return (IntPtr)(((short)wHigh << 16) | (wLow & 0xffff));
        }

        private POINT GetAbsolutePoint(double relativePointX, double relativePointY)
        {
            var p = new POINT {x = (int) (Width*relativePointX), y = (int) (Height*relativePointY)};
            return p;
        }
        #endregion

        private void SilverlightWindowless_HandleDestroyed(object sender, EventArgs e)
        {
            if(m_renderTimer != null)
                m_renderTimer.Stop();
        }

        private void RenderTimer_Tick(object sender, EventArgs e)
        {
            Render();
        }

		public void Render()
		{
            IntPtr handle = HWND;
            if (handle == IntPtr.Zero)
                return;

            /* Send a PAINT message, forcing silverlight to continue rendering */
            SendMessage(handle, WindowsMessages.WM_PAINT, IntPtr.Zero, IntPtr.Zero);
		}

        private int m_silverlightThreadId = 0;

        public static SilverlightWindowless CreateThreadedHost()
        {
            var resetEvent = new ManualResetEvent(false);

            SilverlightWindowless slWindowless = null;

            var t = new Thread((ThreadStart)delegate
            {
                slWindowless = new SilverlightWindowless();

                slWindowless.m_silverlightThreadId = GetCurrentThreadId();
                
                resetEvent.Set();

                Msg msg;

                /* Blocking call to GetMessage  */
                while (GetMessage(out msg, IntPtr.Zero, 0, 0))
                {
                    switch (msg.message)
                    {
                        default:
                            break;
                    }

                    TranslateMessage(ref msg);

                    /* Dispatches the win32 message to a wnd proc.
                     * The real magic is here */
                    DispatchMessage(ref msg);
                }
            });

            t.IsBackground = false;
            t.SetApartmentState(ApartmentState.STA);
            t.Start();

            /* Don't proceed until our Silverlight stuffs has been initialized */
            resetEvent.WaitOne();

            return slWindowless;
        }

        private void CloseHost()
        {
            PostThreadMessage(m_silverlightThreadId, 0x12 /* WM_QUIT */, 0, 0);
        }

        private void EnsureRenderTimer()
        {
            if (m_renderTimer != null)
                return;

            m_renderTimer = new System.Windows.Forms.Timer();
			
            if(m_renderFps != 0)
				m_renderTimer.Interval = 1000 / m_renderFps;

            m_renderTimer.Tick += RenderTimer_Tick;
        }

        /// <summary>
        /// Releases all unmanaged resources
        /// </summary>
        private void ReleaseAllResources()
        {
            if (m_silverlightViewer != null)
            {
                m_silverlightViewer.Release();
                m_silverlightViewer = null;
            }

            ReleaseRenderResources();

            if (m_silverlightControl != null)
            {
                Marshal.ReleaseComObject(m_silverlightControl);
                m_silverlightControl = null;
            }
        }

        /// <summary>
        /// Renders the silverlight root element to a buffer
        /// </summary>
        private void RenderSilverlight()
        {
            /* Make sure we have a buffer to render to */
            EnsureRenderBitmap();

            if (m_silverlightControl != null)
            {
                /* This is how we rendered previously.  Using an hDC */
                /*
                var vo = m_silverlightControl as IViewObject;
                var rect = new RECT { bottom = Width, right = Height };
                OleDraw(vo, 1, IntPtr.Zero, ref rect);
                */

                var dirtyRect = new RECT { bottom = Height, right = Width };

                /* Render the Silverlight root element to our bitmap */
                int hr = m_renderTargetBitmap.Render(GetRootElement(), ref dirtyRect);

                if (hr < 0)
                    return;

                int bufferSize = Width * Height * BITS_PER_PIXEL;

                /* Invoke our new frame event */
                InvokeOnRendered(new RenderedFrameEventArgs(m_pvBits, bufferSize, Width, Height));
            }
        }

        /// <summary>
        /// Ensures there is a bitmap to render to.
        /// </summary>
        private void EnsureRenderBitmap()
        {
            if (!m_controlSizeDirty)
                return;

            /* Free any old bitmap we may of had */
            ReleaseRenderResources();

            /* The SilverlightViewer interface allows us
             * to create new render target bitmaps */
            if (m_silverlightViewer == null)
            {
                var viewerProvider = m_silverlightControl.Content as ISilverlightViewerProvider;
                if (viewerProvider == null)
                    throw new Exception("Could not query for the ISilverlightViewerProvider interface");

                var pViewer = IntPtr.Zero;
                viewerProvider.GetSilverlightViewer(ref pViewer);

                /* SilverlightViewer was NOT built as a COM interface,
                 * so we must use our wrapper class for it */
                m_silverlightViewer = new SilverlightViewer(pViewer);
                Marshal.ReleaseComObject(viewerProvider);
            }

            var pRtb = IntPtr.Zero;

            /* Allocate us some unmanaged mems */
            m_pvBits = Marshal.AllocCoTaskMem(Width * Height * BITS_PER_PIXEL);

            int hr = m_silverlightViewer.CreateRenderTargetBitmap(0 /* Background color */, 
                                                                  0 /* OffsetX */, 
                                                                  0 /* OffsetY */, 
                                                                  Width, 
                                                                  Height, 
                                                                  m_pvBits, 
                                                                  out pRtb);

            if (hr < 0)
                throw new Exception("Could not create Silverlight render target");

            /* Render target bitmap was NOT implemented as a COM interface,
             * so we use our custom wrapper class for it */
            m_renderTargetBitmap = new RenderTargetBitmap(pRtb);

            m_controlSizeDirty = false;
        }

        /// <summary>
        /// Releases our unmanaged resources for rendering
        /// </summary>
        private void ReleaseRenderResources()
        {
            if (m_renderTargetBitmap != null)
            {
                m_renderTargetBitmap.Release();
                m_renderTargetBitmap = null;
            }

            if (m_pvBits != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(m_pvBits);
                m_pvBits = IntPtr.Zero;
            }
        }

        /// <summary>
        /// Gets the root element of the Silverlight application
        /// </summary>
        /// <returns>Pointer to the root element</returns>
        private IntPtr GetRootElement()
        {
            var t = m_silverlightControl.Content.GetType();

            var root = t.InvokeMember(ROOT_ELEMENT_PROPERTY_NAME, 
                                      BindingFlags.GetProperty, 
                                      null,
                                      m_silverlightControl.Content, 
                                      null);

            /* Get the pointer of the returned COM instance*/
            var pElement = Marshal.GetIUnknownForObject(root);

            /* Previous call does an AddRef, so we just cancel that out */
            Marshal.Release(pElement);

            return pElement;
        }

        #region Interactivity
        public void SendLeftButtonDown(double relativeX, double relativeY)
        {
            var p = GetAbsolutePoint(relativeX, relativeY);
            
            var lparam = MakeLParam(p.x, p.y);

            if (InvokeRequired)
                PostMessage(HWND, WindowsMessages.WM_LBUTTONDOWN, new IntPtr(1), lparam);
            else
                SendMessage(WindowsMessages.WM_LBUTTONDOWN, new IntPtr(1), lparam);
        }

        public void SendLeftButtonUp(double relativeX, double relativeY)
        {
            var p = GetAbsolutePoint(relativeX, relativeY);

            var lparam = MakeLParam(p.x, p.y);

            if (InvokeRequired)
                PostMessage(HWND, WindowsMessages.WM_LBUTTONUP, new IntPtr(0), lparam);
            else
                SendMessage(WindowsMessages.WM_LBUTTONUP, new IntPtr(0), lparam);
        }

        public void SendMouseMove(double relativeX, double relativeY)
        {
            var p = GetAbsolutePoint(relativeX, relativeY);
            var lparam = MakeLParam(p.x, p.y);

            if (InvokeRequired)
                PostMessage(HWND, WindowsMessages.WM_MOUSEMOVE, new IntPtr(0), lparam);
            else
                SendMessage(WindowsMessages.WM_MOUSEMOVE, new IntPtr(1), lparam);
        }

        public void SendScroll(int amount)
        {
            var lparam = MakeLParam(0, amount);

            if (InvokeRequired)
                PostMessage(HWND, WindowsMessages.WM_MOUSEWHEEL, new IntPtr(0), lparam);
            else
                SendMessage(HWND, WindowsMessages.WM_MOUSEWHEEL, new IntPtr(0), lparam);
        }

        public void SendMessage(uint message, IntPtr wParam, IntPtr lParam)
        {
            var windowlessInPlace = m_silverlightControl as IOleInPlaceObjectWindowless;
            
            int result = 0;

            /* Pump these messages to our windowless control */
            if(windowlessInPlace != null)
                windowlessInPlace.OnWindowMessage(message, wParam, lParam, out result);
        }

        public void PostMessage(int message, IntPtr wParam, IntPtr lParam)
        {
            PostMessage(HWND, (uint)message, wParam, lParam);
        }
        #endregion

        #region Properties
        /// <summary>
        /// The rate to render the Silverlight
        /// </summary>
        public int RenderFps
        {
            get
            {
                return m_renderFps;
            }
            set
            {
				if(value != 0 && m_renderTimer != null)
				{
					m_renderTimer.Interval = 1000 / value;
					m_renderTimer.Start();
				}
                m_renderFps = value;
            }
        }

        public object SilverlightContent
        {
            get
            {
                if (m_silverlightControl == null)
                    return null;
                
                return m_silverlightControl.Content;
            }
        }

        /// <summary>
        /// The url to the Silverlight .XAP application
        /// </summary>
        public string Source
        {
            get
            {
                if (m_silverlightControl == null)
                    return m_source;

                return m_silverlightControl.Source;
            }
            set
            {
                m_source = value;
                if(m_silverlightControl != null)
                    m_silverlightControl.Source =value;
            }
        }

        /// <summary>
        /// XAML to provide to Silverlight to render
        /// </summary>
        public string Xaml
        {
            get { return m_xaml; }
            set
            {
                const string LOAD_XAML_SOURCE_URI = "#source";
                const string CREATE_FROM_XAML_METHOD_NAME = "CreateFromXaml";
                const string CONTENT_PROPERTY = "Content";

                m_xaml = value;

                if ((!string.IsNullOrEmpty(Source) && !Source.Contains(LOAD_XAML_SOURCE_URI)) || m_silverlightControl == null)
                    return;

                if (m_silverlightControl.Source != LOAD_XAML_SOURCE_URI)
                    m_silverlightControl.Source = LOAD_XAML_SOURCE_URI;

                object instantiatedXamlElement = null;
                object rootElement = null;

                try
                {
                    var silverlightContentType = m_silverlightControl.Content.GetType();

                    instantiatedXamlElement = silverlightContentType.InvokeMember(CREATE_FROM_XAML_METHOD_NAME,
                                                                                  BindingFlags.InvokeMethod,
                                                                                  null,
                                                                                  m_silverlightControl.Content,
                                                                                  new object[] { m_xaml });

                    rootElement = silverlightContentType.InvokeMember(ROOT_ELEMENT_PROPERTY_NAME,
                                                                     BindingFlags.GetProperty,
                                                                     null,
                                                                     m_silverlightControl.Content,
                                                                     null);

                    var rootType = rootElement.GetType();

                    rootType.InvokeMember(CONTENT_PROPERTY, BindingFlags.SetProperty, null, rootElement, new[] { instantiatedXamlElement });
                }
                finally
                {
                    if (instantiatedXamlElement != null)
                        Marshal.ReleaseComObject(instantiatedXamlElement);

                    if (rootElement != null)
                        Marshal.ReleaseComObject(rootElement);
                }
            }
        }

        /// <summary>
        /// The base url used for when a Silverlight 
        /// application requests a file be downloaded 
        /// </summary>
        public string BaseUrl
        {
            get { return m_baseUrl; }
            set { m_baseUrl = value; }
        }

        /// <summary>
        /// Handle of control.  This is a cached value
        /// from the Handle property for easy cross thread access
        /// </summary>
        public IntPtr HWND
        {
            get { return m_handle; }
            set { m_handle = value; }
        }

        #endregion

        #region Overrides
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);

            m_controlSizeDirty = true;

            if (m_silverlightControl == null) return;

            var rect = RECT.FromRectangle(ClientRectangle);
            var oleObject = m_silverlightControl as IOleInPlaceObject;

            /* Inform the windowless control that the size has changed */
            if(oleObject != null)
                oleObject.SetObjectRects(ref rect, ref rect);
        }

        protected override void OnGotFocus(EventArgs e)
        {
            if(m_silverlightControl != null)
            {
                var oleObject = m_silverlightControl as IOleObject;

                if (oleObject == null)
                    return;

                var msg = new MSG();
                var rect = RECT.FromRectangle(ClientRectangle);
                oleObject.DoVerb(-4, ref msg, this, 0, Handle, ref rect);
            }

            base.OnGotFocus(e);
        }

        protected override void OnHandleCreated(EventArgs e)
        {
            HWND = Handle;

            /* Create a new Silverlight COM instance */
            m_silverlightControl = new XcpControl();

            var persistPropertyBag = m_silverlightControl as IPersistPropertyBag;

            if (persistPropertyBag == null)
                throw new Exception("Could not query IPersistPropertyBag interface");
            
            /* This is needed because our property bag informs Silverlight that we
             * want to be rendering as Windowless */
            persistPropertyBag.Load(new SilverlightPropertyBag(), null);

            var oleObject = m_silverlightControl as IOleObject;
            if (oleObject == null)
                throw new Exception("Could not query IOleObject");

            /* Let the Silverlight control know about us */
            oleObject.SetClientSite(this);
            oleObject.SetHostNames("Container", string.Empty);

            /* Blank args for DoVerb call */
            var rect = new RECT();
            var msg = new MSG();

            /* Inplace activate our windowless Silverlight */
            int hr = oleObject.DoVerb(OLEIVERB_INPLACE_ACTIVATE, ref msg, this, 0, Handle, ref rect);
            if (hr < 0)
                throw new Exception("Could not inplace activate the Silverlight control");

            m_controlSizeDirty = true;

            if (m_source != string.Empty)
                Source = m_source;

            /* Ensure our render timer has been configured */
            EnsureRenderTimer();

            /* Start rendering */
			if(m_renderFps != 0)
				m_renderTimer.Start();

            base.OnHandleCreated(e);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            /* Do nothing.  Don't call base OnPaint.  It shouldn't be needed */
            e.Dispose();
        }
        
        public override int OnInPlaceActivate()
        {
            return S_OK;
        }

        public override int InvalidateRect(RECT[] pRect, int fErase)
        {
            return S_OK;
        }

        protected override void WndProc(ref Message m)
        {
            if(m_silverlightControl != null)
            {
                var windowlessInPlace = m_silverlightControl as IOleInPlaceObjectWindowless;
                
                if(windowlessInPlace != null)
                {
                    
                    if (m.Msg == WindowsMessages.WM_LBUTTONDOWN || 
                        m.Msg == WindowsMessages.WM_LBUTTONUP || 
                        m.Msg == WindowsMessages.WM_MOUSEMOVE || 
                        m.Msg == WindowsMessages.WM_SETCURSOR ||
                        m.Msg == WindowsMessages.WM_MOUSEWHEEL ||
                        m.Msg == WindowsMessages.WM_KEYUP ||
                        m.Msg == WindowsMessages.WM_KEYDOWN ||
                        m.Msg == WindowsMessages.WM_LBUTTONDBLCLK ||
                        m.Msg == WindowsMessages.WM_CHAR ||
                        m.Msg == WindowsMessages.WM_ACTIVATE)
                    {
                        int result = 0;
                        /* Pump these messages to our windowless control */
                        windowlessInPlace.OnWindowMessage((uint)m.Msg, m.WParam, m.LParam, out result);
                    }

                    if (m.Msg == WindowsMessages.WM_PAINT)
                    {
                        RenderSilverlight();
                        return;
                    }
                }
            }

            base.WndProc(ref m);
        }

        protected override void Dispose(bool disposing)
        {
            if(InvokeRequired)
            {
                BeginInvoke((Action) delegate
                {
                    /* Only run CloseHost if Dispose called 
                     * from foreign thread */
                    CloseHost();
                    Dispose(disposing);
                });
                return;
            }

            if (m_renderTimer != null)
            {
                m_renderTimer.Stop();
                m_renderTimer = null;
            }

            ReleaseAllResources();
            base.Dispose(disposing);
        }
        #endregion

        #region IXcpControlHost Members

        public uint GetHostOptions()
        {
            return (uint)(XcpHostOptions.EnableCrossDomainDownloads | XcpHostOptions.EnableScriptableObjectAccess | XcpHostOptions.EnableHtmlDomAccess);
        }

        public void NotifyLoaded()
        {
        }

        public void NotifyError(string strFilename, string bstrSource, int nLine, int nColumn)
        {
            InvokeSilverlightError(new SilverlightErrorEventArgs{Filename = strFilename, Source = bstrSource, Line = nLine, Column = nColumn});
        }

        public object InvokeHandler(string bstrName, object varArg1, object varArg2)
        {
            Debug.WriteLine(string.Format("IXcpControlHost::InvokeHandler: {0}", bstrName));
            return null;
        }

        public string GetBaseUrl()
        {
            if (string.IsNullOrEmpty(Source))
                return string.Empty;

            return BaseUrl;
        }

        public string GetNamedSource(string bstrSourceName)
        {
            return XAML_SOURCE_CONTAINER;
        }

        public int DownloadUrl(string bstrUrl, IXcpControlDownloadCallback pCallback, out IStream ppStream)
        {
            if (bstrUrl == Source)
                goto end;

            ThreadPool.QueueUserWorkItem(delegate
            {
                var client = new WebClient();

                try
                {
                    var data = client.DownloadData(bstrUrl);

                    BeginInvoke((Action)delegate
                    {
                        var memstream = new NativeStream(new MemoryStream(data));
                        pCallback.OnUrlDownloaded(S_OK, memstream);
                        Marshal.ReleaseComObject(pCallback);
                    });
                }
                catch (Exception)
                {
                    BeginInvoke((Action) delegate
                    {
                        pCallback.OnUrlDownloaded(S_FALSE, null);
                        Marshal.ReleaseComObject(pCallback);
                    });
                }
            });

end:
            ppStream = null;
            return S_FALSE;
        }

        #endregion

        #region IServiceProvider Members

        public int QueryService(ref Guid guidService, ref Guid riid, out IntPtr ppvObject)
        {
            ppvObject = IntPtr.Zero;

            if(guidService == typeof(IXcpControlHost).GUID && riid == typeof(IXcpControlHost).GUID)
            {
                ppvObject = Marshal.GetComInterfaceForObject(this, typeof (IXcpControlHost));
                return S_OK;
            }
            return E_NOINTERFACE;
        }

        #endregion
    }
}
